<template>
  <div class="simple-upload-container">
    <div class="total-progress">
      <div class="btns">
        <a-button-group>
          <a-button :disabled="changeDisabled">
            <i class="el-icon-upload2 el-icon--left" size="mini"></i>选择文件
            <input
              v-if="!changeDisabled"
              type="file"
              :multiple="multiple"
              class="select-file-input"
              :accept="accept"
              οnclick="f.outerHTML=f.outerHTML"
              @change="handleFileChange"
            />
          </a-button>
          <a-button :disabled="uploadDisabled" @click="handleUpload"><i class="el-icon-upload el-icon--left" size="mini"></i>上传</a-button>
          <a-button :disabled="pauseDisabled" @click="handlePause"><i class="el-icon-video-pause el-icon--left" size="mini"></i>暂停</a-button>
          <a-button :disabled="resumeDisabled" @click="handleResume"><i class="el-icon-video-play el-icon--left" size="mini"></i>恢复</a-button>
          <a-button :disabled="clearDisabled" @click="clearFiles"><i class="el-icon-video-play el-icon--left" size="mini"></i>清空</a-button>
        </a-button-group>
        <slot name="header"></slot>
      </div>
    </div>
    <div class="file-list">
      <div v-if="uploadFiles.length" accordion>
        <div v-for="(item, index) in uploadFiles" :key="index">
          <div class="progress-box">
            <div class="list-item">
              <div class="item-name">
                <span>{{ index + 1 }}：名称：{{ item.name }}</span>
              </div>
              <div class="item-size">大小：{{ item.size | transformByte }}</div>
              <div v-if="item.hashProgress !== 100" class="item-progress">
                <span>{{ status === 'wait' ? '准备读取文件' : '正在读取文件' }}：</span>
                <a-progress :percent="item.hashProgress" />
              </div>
              <div v-else class="item-progress"><span>文件进度：</span><a-progress :percent="item.uploadProgress" /></div>
              <div class="item-status">
                <i :class="fileStatuIcon(item.status)"></i>
                {{ item.status | fileStatus }}
              </div>
            </div>
          </div>
          <div class="item-chunk-box">
            <ul class="list">
              <li class="title">
                <span class="index">#</span>
                <span class="clip">切片md5</span>
                <span class="size">大小</span>
                <span class="status">是否完成</span>
                <span class="progress">进度</span>
              </li>
            </ul>
            <ul class="list">
              <li v-for="(fileClip, mi) in item.chunkList" :key="mi">
                <span class="index">{{mi}}</span>
                <span class="clip">{{fileClip.hash}}</span>
                <span class="size">{{fileClip.size | transformByte}}</span>
                <span class="status">{{fileClip.uploaded ? '完成' : '进行中' }}</span>
                <span class="progress">
                  <a-progress v-if="!fileClip.status || fileClip.status === 'wait'" :percent="fileClip.progress" />
                  <a-progress v-else :percent="fileClip.progress" :status="fileClip.status" />
                </span>
              </li>
            </ul>
          </div>
        </div>
      </div>
      <slot name="tip"></slot>
    </div>
  </div>
</template>
<script>
/* eslint-disable */
import { saveObjArr, getObjArr, clearLocalStorage } from '@/utils/localstorage'
import axios, { CancelToken } from 'axios'

var instance = axios.create({})

var chunkSize = 10 * 1024 * 1024 // 切片大小
var fileIndex = 0 // 当前正在被遍历的文件下标

// 所有文件状态
const Status = {
  wait: 'wait',
  pause: 'pause',
  uploading: 'uploading',
  hash: 'hash',
  error: 'error',
  done: 'done'
}

// 单个文件的状态
const fileStatus = {
  wait: 'wait',
  uploading: 'uploading',
  success: 'success',
  error: 'error',
  secondPass: 'secondPass',
  pause: 'pause',
  resume: 'resume'
}
// 单个文件的状态 对应描述
const fileStatusStr = {
  wait: '待上传',
  uploading: '上传中',
  success: '成功',
  error: '失败',
  secondPass: '已秒传',
  pause: '暂停',
  resume: '恢复'
}

export default {
  name: 'SimpleUploaderContainer',
  filters: {
    transformByte (size) {
      if (!size) {
        return '0B'
      }

      var num = 1024.0 // byte

      if (size < num) {
        return size + 'B'
      }
      if (size < Math.pow(num, 2)) {
        return (size / num).toFixed(2) + 'K'
      } // kb
      if (size < Math.pow(num, 3)) {
        return (size / Math.pow(num, 2)).toFixed(2) + 'M'
      } // M
      if (size < Math.pow(num, 4)) {
        return (size / Math.pow(num, 3)).toFixed(2) + 'G'
      } // G
      return (size / Math.pow(num, 4)).toFixed(2) + 'T' // T
    },
    // 单个文件状态格式化
    fileStatus (status) {
      return fileStatusStr[fileStatus[status]]
    }
  },
  props: {
    headers: {
      type: Object,
      default: null
    },
    beforeUpload: {
      type: Function,
      default: null
    },
    accept: {
      type: String,
      default: ''
    },
    // 上传文件时携带的参数
    uploadArguments: {
      type: Object,
      default: () => {
        return {}
      }
    },
    // 是否传递cookie
    withCredentials: {
      type: Boolean,
      default: false
    },
    // 文件个数
    limit: {
      type: Number,
      default: 0
    },
    // 文件超出个数限制时的钩子
    onExceed: {
      type: Function,
      default: () => {}
    },
    multiple: {
      type: Boolean,
      default: true
    },
    // axios baseUrl
    baseUrl: {
      type: String,
      default: ''
    },
    // 切片大小
    chunkSize: {
      type: Number,
      default: 10 * 1024 * 1024
    },
    // 上传并发数
    threads: {
      type: Number,
      default: 3
    },
    // 错误重试次数
    chunkRetry: {
      type: Number,
      default: 3
    }
  },
  data: () => ({
    uploadFiles: [],
    worker: null,
    cancels: [], // 存储要取消的请求
    tempThreads: 3,
    // 默认状态
    status: Status.wait
  }),
  computed: {
    changeDisabled () {
      return ![Status.wait, Status.done].includes(this.status)
    },
    uploadDisabled () {
      return !this.uploadFiles.length || [Status.pause, Status.done, Status.uploading, Status.hash].includes(this.status)
    },
    pauseDisabled () {
      return [Status.hash, Status.wait, Status.done, Status.pause].includes(this.status)
    },
    resumeDisabled () {
      return ![Status.pause].includes(this.status)
    },
    clearDisabled () {
      return !this.uploadFiles.length
    },
    fileStatuIcon (status) {
      return function (status) {
        var className = ''
        switch (status) {
          case 'uploading':
            className = 'el-icon-loading'
            break
          case 'success':
          case 'secondPass':
            className = 'el-icon-circle-check'
            break
          case 'error':
            className = 'el-icon-circle-close'
            break
        }
        return className + ' el-icon--left'
      }
    }
  },
  destroyed () {
    this.clearFiles()
  },
  created () {
    this.setAxios()
    this.tempThreads = this.threads
  },
  methods: {
    handleFileChange (e) {
      const files = e.target.files
      console.log('选中文件 file', files)
      if (!files) return

      fileIndex = 0 // 重置文件下标
      console.log('选中文件 this.uploadFiles', this.uploadFiles)
      // 判断文件选择的个数
      if (this.limit && files.length > this.limit) {
        this.onExceed && this.onExceed(files)
        return
      }

      this.status = Status.wait

      const postFiles = Array.prototype.slice.call(files)
      console.log('选中文件 postFiles', postFiles)
      postFiles.forEach((item) => {
        this.handleStart(item)
      })
    },
    handleStart (rawFile) {
      // 初始化部分自定义属性
      rawFile.status = fileStatus.wait
      rawFile.chunkList = []
      rawFile.uploadProgress = 0
      rawFile.fakeUploadProgress = 0 // 假进度条，处理恢复上传后，进度条后移的问题
      rawFile.hashProgress = 0

      if (this.beforeUpload) {
        const before = this.beforeUpload(rawFile)
        if (before && before.then) {
          before.then((res) => {
            this.uploadFiles.push(rawFile)
          })
        }
      }

      if (!this.beforeUpload) {
        this.uploadFiles.push(rawFile)
      }
    },
    // 上传
    async handleUpload () {
      console.log('上传', this.uploadFiles)
      if (!this.uploadFiles) return
      this.status = Status.uploading
      const filesArr = this.uploadFiles
      for (let i = 0; i < filesArr.length; i++) {
        fileIndex = i
        if (['secondPass', 'success', 'error'].includes(filesArr[i].status)) {
          console.log('跳过已上传成功或已秒传的或失败的')
          continue
        }

        const fileChunkList = this.createFileChunk(filesArr[i])

        // 若不是恢复，再进行hash计算
        if (filesArr[i].status !== 'resume') {
          this.status = Status.hash
          // hash校验，是否为秒传
          filesArr[i].hash = await this.calculateHash(fileChunkList)

          // 若清空或者状态为等待，则跳出循环
          if (this.status === Status.wait) {
            console.log('若清空或者状态为等待，则跳出循环')
            break
          }

          console.log('上传 hash', filesArr[i].hash)
        }

        this.status = Status.uploading

        const verifyRes = await this.verifyUpload(filesArr[i].name, filesArr[i].hash)
        if (verifyRes.data.presence) {
          filesArr[i].status = fileStatus.secondPass
          filesArr[i].uploadProgress = 100
          this.isAllStatus()
        } else {
          console.log('开始上传文件----》', filesArr[i].name)
          filesArr[i].status = fileStatus.uploading

          const getChunkStorage = this.getChunkStorage(filesArr[i].hash)
          filesArr[i].fileHash = filesArr[i].hash // 文件的hash，合并时使用
          filesArr[i].chunkList = fileChunkList.map(({ file }, index) => ({
            fileHash: filesArr[i].hash,
            fileName: filesArr[i].name,
            index,
            hash: filesArr[i].hash + '-' + index,
            chunk: file,
            size: file.size,
            uploaded: getChunkStorage && getChunkStorage.includes(index), // 标识：是否已完成上传
            progress: getChunkStorage && getChunkStorage.includes(index) ? 100 : 0,
            status: getChunkStorage && getChunkStorage.includes(index) ? 'success' : 'wait' // 上传状态，用作进度状态显示
          }))

          this.$set(filesArr, i, filesArr[i])

          console.log('上传  this.chunkData', filesArr[i])
          await this.uploadChunks(filesArr[i])
        }
      }
    },
    // 将切片传输给服务端
    async uploadChunks (data) {
      console.log('发送切片', data)
      var chunkData = data.chunkList
      return new Promise(async (resolve, reject) => {
        const requestDataList = chunkData
          .filter(({ uploaded }) => !uploaded)
          .map(({ fileHash, chunk, fileName, index }) => {
            const formData = new FormData()
            formData.append('md5', fileHash)
            formData.append('file', chunk)
            formData.append('fileName', index) // 文件名使用切片的下标

            return { formData, index, fileName }
          })

        console.log('将切片传输给服务端 requestDataList', requestDataList)

        try {
          const ret = await this.sendRequest(requestDataList, chunkData)
          console.log('将切片传输给服务端 chunkData', chunkData)
          console.log('ret', ret)
        } catch (error) {
          // 上传有被reject的
          this.$message.error('亲 上传失败了,考虑重试下呦' + error)
          return
        }

        // 合并切片
        const isUpload = chunkData.some((item) => item.uploaded === false)
        console.log('created -> isUpload', isUpload)
        if (isUpload) {
          alert('存在失败的切片')
        } else {
          // 执行合并
          try {
            await this.mergeRequest(data)
            resolve()
          } catch (error) {
            reject()
          }
        }
      })
    },
    // 并发处理
    sendRequest (forms, chunkData) {
      console.log('并发处理 -> forms', forms)
      console.log('并发处理 -> chunkData', chunkData)
      var finished = 0
      const total = forms.length
      const that = this
      const retryArr = [] // 数组存储每个文件hash请求的重试次数，做累加 比如[1,0,2],就是第0个文件切片报错1次，第2个报错2次

      return new Promise((resolve, reject) => {
        const handler = () => {
          console.log('handler -> forms', forms)
          if (forms.length) {
            // 出栈
            const formInfo = forms.shift()

            const formData = formInfo.formData
            const index = formInfo.index

            instance
              .post('fileChunk', formData, {
                onUploadProgress: that.createProgresshandler(chunkData[index]),
                cancelToken: new CancelToken((c) => this.cancels.push(c)),
                timeout: 0
              })
              .then((res) => {
                console.log('handler -> res', res)
                // 更改状态
                chunkData[index].uploaded = true
                chunkData[index].status = 'success'

                // 存储已上传的切片下标
                this.addChunkStorage(chunkData[index].fileHash, index)

                finished++
                handler()
              })
              .catch((e) => {
                // 若状态为暂停或等待，则禁止重试
                console.log('handler -> this.status', this.status)
                if ([Status.pause, Status.wait].includes(this.status)) return

                console.warn('出现错误', e)
                console.log('handler -> retryArr', retryArr)
                if (typeof retryArr[index] !== 'number') {
                  retryArr[index] = 0
                }

                // 更新状态
                chunkData[index].status = 'warning'

                // 累加错误次数
                retryArr[index]++

                // 重试3次
                if (retryArr[index] >= this.chunkRetry) {
                  console.warn(' 重试失败--- > handler -> retryArr', retryArr, chunkData[index].hash)
                  return reject('重试失败', retryArr)
                }

                console.log('handler -> retryArr[finished]', `${chunkData[index].hash}--进行第 ${retryArr[index]} '次重试'`)
                console.log(retryArr)

                this.tempThreads++ // 释放当前占用的通道

                // 将失败的重新加入队列
                forms.push(formInfo)
                handler()
              })
          }

          if (finished >= total) {
            resolve('done')
          }
        }

        // 控制并发
        for (let i = 0; i < this.tempThreads; i++) {
          handler()
        }
      })
    },
    // 通知服务端合并切片
    mergeRequest (data) {
      return new Promise((resolve, reject) => {
        const obj = {
          md5: data.fileHash,
          fileName: data.name,
          fileChunkNum: data.chunkList.length,
          ...this.uploadArguments
        }

        instance
          .post('fileChunk/merge', obj, {
            timeout: 0
          })
          .then((res) => {
            // 清除storage
            if (res.data.code === 2000) {
              data.status = fileStatus.success
              console.log('通知服务端合并切片 data', data)
              clearLocalStorage(data.fileHash)
              // this.$message.success('上传成功');
              // 判断是否所有都成功上传
              this.isAllStatus()
              resolve()
            } else {
              // 文件块数量不对，清除缓存
              clearLocalStorage(data.fileHash)
              data.status = fileStatus.error
              this.status = Status.wait
              resolve()
            }
          })
          .catch((err) => {
            console.log('通知服务端合并切片 err', err)
            data.status = fileStatus.error
            reject()
          })
      })
    },
    // 创建文件切片
    createFileChunk (file, size = chunkSize) {
      console.log(file, 'wenjian', size)
      const fileChunkList = []
      var count = 0
      while (count < file.size) {
        fileChunkList.push({
          file: file.slice(count, count + size)
        })
        count += size
      }
      console.log('创建文件切片 fileChunkList', fileChunkList)
      return fileChunkList
    },

    // 暂停上传
    handlePause () {
      this.status = Status.pause
      if (this.uploadFiles.length) {
        const currentFile = this.uploadFiles[fileIndex]
        currentFile.status = fileStatus.pause
        // 将当前进度赋值给假进度条
        currentFile.fakeUploadProgress = currentFile.uploadProgress
        console.log('暂停上传 currentFile', currentFile)
      }
      while (this.cancels.length > 0) {
        this.cancels.pop()('取消请求')
      }
    },
    // 恢复上传
    handleResume () {
      this.status = Status.uploading
      console.log('恢复上传 this.uploadFiles[fileIndex]', this.uploadFiles[fileIndex])
      this.uploadFiles[fileIndex].status = fileStatus.resume
      this.handleUpload()
    },
    // 文件上传之前的校验： 校验文件是否已存在
    verifyUpload (fileName, fileHash) {
      return new Promise((resolve) => {
        const obj = {
          md5: fileHash,
          fileName,
          ...this.uploadArguments
        }
        instance
          .get('fileChunk/presence', { params: obj })
          .then((res) => {
            console.log('文件上传之前的校验： 校验文件是否已存在 res', res)
            resolve(res.data)
          })
          .catch((err) => {
            console.log('文件上传之前的校验： 校验文件是否已存在 err', err)
          })
      })
    },
    // 生成文件 hash（web-worker）
    calculateHash (fileChunkList) {
      console.log('生成文件 hash（web-worker） -> fileChunkList, 需要生成hash的文件', fileChunkList)
      return new Promise((resolve) => {
        this.worker = new Worker('./hash.js')
        this.worker.postMessage({ fileChunkList })
        this.worker.onmessage = (e) => {
          const { percentage, hash } = e.data
          if (this.uploadFiles[fileIndex]) {
            this.uploadFiles[fileIndex].hashProgress = Number(percentage.toFixed(0))
            this.$set(this.uploadFiles, fileIndex, this.uploadFiles[fileIndex])
          }

          if (hash) {
            resolve(hash)
          }
        }
      })
    },
    // 切片上传进度
    createProgresshandler (item) {
      console.log('切片上传进度 -> item', item)
      return (p) => {
        item.progress = parseInt(String((p.loaded / p.total) * 100))
        this.fileProgress()
      }
    },
    // 文件总进度
    fileProgress () {
      const currentFile = this.uploadFiles[fileIndex]
      if (currentFile) {
        const uploadProgress = currentFile.chunkList.map((item) => item.size * item.progress).reduce((acc, cur) => acc + cur)
        const currentFileProgress = parseInt((uploadProgress / currentFile.size).toFixed(2))

        // 真假进度条处理--处理进度条后移
        if (!currentFile.fakeUploadProgress) {
          currentFile.uploadProgress = currentFileProgress
          this.$set(this.uploadFiles, fileIndex, currentFile)
        } else if (currentFileProgress > currentFile.fakeUploadProgress) {
          currentFile.uploadProgress = currentFileProgress
          this.$set(this.uploadFiles, fileIndex, currentFile)
        }
      }
    },
    // 存储已上传完成的切片下标
    addChunkStorage (name, index) {
      const data = [index]
      console.log('存储已上传完成的切片下标 -> name, data', name, data)
      const arr = getObjArr(name)
      if (arr) {
        saveObjArr(name, [...arr, ...data])
      } else {
        saveObjArr(name, data)
      }
    },
    // 获取已上传完成的切片下标
    getChunkStorage (name) {
      return getObjArr(name)
    },
    // axios 设置axios参数
    setAxios () {
      if (!this.headers) return
      for (const i in this.headers) {
        instance.defaults.headers.common[i] = this.headers[i]
      }

      // 是否携带cookie
      if (this.withCredentials) {
        instance.defaults.withCredentials = true
      }

      // 设置baseUrl
      if (this.baseUrl) {
        instance.defaults.baseURL = this.baseUrl
      }

      // 设置切片大小
      chunkSize = this.chunkSize
    },
    // 清空文件
    clearFiles () {
      console.log('清空文件')
      fileIndex = 0
      this.handlePause()

      this.worker && this.worker.terminate() // 中断worker
      this.status = Status.wait

      Object.assign(this.$data, this.$options.data()) // 重置data所有数据
    },
    // 判断是否都已完成上传
    isAllStatus () {
      const isAllSuccess = this.uploadFiles.every((item) => ['success', 'secondPass', 'error'].includes(item.status))
      console.log('通知服务端合并切片 isAllSuccess', isAllSuccess)
      if (isAllSuccess) {
        this.status = Status.done
        this.$emit('success')
      }
    }
  }
}
</script>
<style scoped lang="scss">
.simple-upload-container {
  width: 100%;
  border: 1px solid #d2d2d2;
  border-radius: 4px;
  background-color: #fff;
  padding-bottom: 15px;
  padding: 10px;
  .progress-box {
    width: 100%;
  }
  .total-progress {
    margin-bottom: 15px;
    .btns {
      position: relative;
      .select-file-input {
        position: absolute;
        display: inline-block;
        left: 0;
        top: 0;
        border: none;
        opacity: 0;
        width: 96px;
        height: 28px;
      }
    }
  }
  .file-list {
    .list-item {
      padding: 8px 10px;
      display: flex;
      justify-content: center;
      justify-items: center;
      line-height: 25px;
      position: relative;
      &:hover .item-chunk-box {
        display: block;
      }
      div {
        flex: 1;
        margin-top: 6px;
      }
      .item-name {
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
        margin-right: 6px;
        .svg-icon {
          font-size: 22px;
          vertical-align: sub;
        }
      }
      .item-status {
        flex: 0 0 10%;
        text-align: center;
        text-align: left;
        .el-icon-circle-check {
          color: #67c23a;
        }
        .el-icon-circle-close {
          color: #f00;
        }
      }
      // .item-chunk-box {
      //   // display: none;
      //   // transition: all 3s;
      //   // position: absolute;
      //   // top: 0;
      //   // left: 40px;
      //   // z-index: 10;
      // }
      .item-progress {
        display: flex;
        flex: 0 0 60%;
        align-items: center;
      }
    }
  }

  .upload-tip {
    font-size: 12px;
    color: #606266;
    margin-top: 7px;
  }

  .a-progress {
    width: 80%;
    display: inline-block;
  }
  >>> .div-item__header {
    height: auto;
  }
}
 .list {
    max-height: 200px;
    overflow-y: auto;
    // background: red;
    list-style: none;
    // border: 1px solid #333;
    border-bottom: none;
    li {
      list-style: none;
      display: flex;
      border-bottom: 1px solid #333;
      &.title {
         border-bottom: none;
      }
      span {
        // border-right: 1px solid #333;
        padding: 5px 10px;
      }
      .index {
        flex: 1 1 10%;
      }
      .clip {
        flex: 1 1 20%;
      }
      .size {
        flex: 1 1 10%;
      }
      .status {
        flex: 1 1 10%;
      }
      .progress {
        flex: 1 1 50%;
        border-right: none;
      }
    }
  }
</style>
